﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using WorldSimulator.PhysicsEngine;
using WorldSimulator;
using System.Threading.Tasks;

namespace WorldSimulator.PhysicEngine
{
	public class MyPhysicsEngine : IPhysicsEngine
	{
		/// <summary>
		/// Mapping of a physical object to its physics data (ie. gravity)
		/// </summary>
		protected Dictionary<IPhysicalObject, MyPhysicsData> _physicalObjects;

		/// <summary>
		/// List of actions that can be parallelized to calculate the physics (needed for multicore CPUs)
		/// </summary>
		protected List<Action> _calculationInstructionCache = null;

		//// CONSTRUCTION

		public MyPhysicsEngine()
		{
			this._physicalObjects = new Dictionary<IPhysicalObject, MyPhysicsData>();
		}

		//// PUBLIC METHODS

		/// <summary>
		/// Registers the objects for physics calculation
		/// </summary>
		/// <param name="objects">Objects to register</param>
		public void RegisterObjects(IEnumerable<IPhysicalObject> objects)
		{
			foreach (var obj in objects)
				this.RegisterObject(obj);

			this._calculationInstructionCache = null;
		}

		/// <summary>
		/// Unregisters the objects for physics calculation
		/// </summary>
		/// <param name="objects">Objects to unregister</param>
		public void UnregisterObjects(IEnumerable<IPhysicalObject> objects)
		{
			foreach (var obj in objects)
				this.UnregisterObject(obj);

			this._calculationInstructionCache = null;
		}

		/// <summary>
		/// Registers an object for physics calculation
		/// </summary>
		/// <param name="obj">Physical object</param>
		public void RegisterObject(IPhysicalObject obj)
		{
			if (!this._physicalObjects.ContainsKey(obj))
				this._physicalObjects.Add(obj, new MyPhysicsData());

			this._calculationInstructionCache = null;
		}

		/// <summary>
		/// Unregisters an object for physics calculation
		/// </summary>
		/// <param name="obj">Physical object</param>
		public void UnregisterObject(IPhysicalObject obj)
		{
			if (this._physicalObjects.ContainsKey(obj))
				this._physicalObjects.Remove(obj);

			this._calculationInstructionCache = null;
		}

		/// <summary>
		/// Calculates the physically acting forces and applies them to the momentum of the objects
		/// </summary>
		/// <param name="timeDeltaInSeconds">timeDelta in seconds</param>
		public void ApplyPhysics(double timeDeltaInSeconds)
		{
			if (this._calculationInstructionCache == null)
				this.UpdateCalculationCache();

			//Calculate inertias
			Parallel.ForEach(this._calculationInstructionCache, x => x());

			//Apply gravity
			foreach (var obj in this._physicalObjects.Keys)
			{
				obj.Velocity += this._physicalObjects[obj].Gravity * timeDeltaInSeconds;

				//Reset the inertia
				this._physicalObjects[obj].Reset();
			}
		}

		/// <summary>
		/// Moves the objects based on the currently acting forces
		/// </summary>
		/// <param name="timeDeltaInSeconds">timeDelta in seconds</param>
		public void MoveObjects(double timeDeltaInSeconds)
		{
			foreach (var obj in this._physicalObjects.Keys)
				obj.Location += obj.Velocity * timeDeltaInSeconds;
		}

		/// <summary>
		/// Disposes this instance
		/// </summary>
		public void Dispose()
		{
			this._physicalObjects = null;
		}
		
		//// PRIVATE METHODS

		/// <summary>
		/// Updates the list of parallelizable instructions need to be executed to calculate the physics
		/// </summary>
		private void UpdateCalculationCache()
		{
			var keys = this._physicalObjects.Keys.ToArray();
			this._calculationInstructionCache = new List<Action>();
		
			for (int i = 0; i < keys.Length; i++)
			{
				for (int j = i + 1; j < keys.Length; j++)
				{
					int ii = i;
					int jj = j;
					this._calculationInstructionCache.Add(() => this.CalculateForces(keys[ii], keys[jj]));
				}
			}
		}

		/// <summary>
		/// Calculates the interactive gravity forces for the specified objects
		/// </summary>
		/// <param name="obj1">first object</param>
		/// <param name="obj2">second object</param>
		private void CalculateForces(IPhysicalObject obj1, IPhysicalObject obj2)
		{
			double dist = obj1.Location.DistanceTo(obj2.Location);

			//Objects are colliding
			if (dist < (obj1.Radius + obj2.Radius))
					return;

			//todo 100000-multiplication
			double F1 = PhysicalConstants.NewtonsConstant * obj2.Mass / dist * 1000000;
			double F2 = PhysicalConstants.NewtonsConstant * obj1.Mass / dist * 1000000;

			MyVector3D winkelObj1 = (obj2.Location - obj1.Location).ToNormalized();
			MyVector3D winkelObj2 = (obj1.Location - obj2.Location).ToNormalized();

			this._physicalObjects[obj1].Gravity += winkelObj1 * F1 / obj1.Mass;
			this._physicalObjects[obj2].Gravity += winkelObj2 * F2 / obj2.Mass;
		}
	}
}
